package org.webpieces.nio.impl.cm.basic;
import java.io.IOException;
import java.net.PortUnreachableException;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectionKey;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import org.webpieces.data.api.BufferPool;
import org.webpieces.nio.api.channels.Channel;
import org.webpieces.nio.api.exceptions.NioException;
import org.webpieces.nio.api.handlers.DataListener;
import org.webpieces.nio.api.testutil.nioapi.Select;
public final class Helper {
private static final Logger apiLog = LoggerFactory.getLogger(DataListener.class);
private static final Logger log = LoggerFactory.getLogger(Helper.class);
//private static BufferHelper helper = ChannelManagerFactory.bufferHelper(null);
private static boolean logBufferNextRead = false;
private Helper() {}
public static String opType(int ops) {
String retVal = "";
if((ops & SelectionKey.OP_ACCEPT) > 0)
retVal+="A";
if((ops & SelectionKey.OP_CONNECT) > 0)
retVal+="C";
if((ops & SelectionKey.OP_READ) > 0)
retVal+="R";
if((ops & SelectionKey.OP_WRITE) > 0)
retVal+="W";
return retVal;
}
public static void processKeys(Set<SelectionKey> keySet, SelectorManager2 mgr, BufferPool pool) {
Iterator<SelectionKey> iter = keySet.iterator();
while (iter.hasNext()) {
SelectionKey key = null;
try {
key = iter.next();
final SelectionKey current = key;
log.trace(() -> current.attachment()+" ops="+Helper.opType(current.readyOps())
+" acc="+current.isAcceptable()+" read="+current.isReadable()+" write"+current.isWritable());
processKey(key, mgr, pool);
// } catch(CancelledKeyException e) {
// log.log(Level.INFO, "Cancelled key may be normal", e);
} catch(IOException e) {
log.error(key.attachment()+"Processing of key failed, closing channel", e);
try {
if(key != null)
key.channel().close();
} catch(Throwable ee) {
log.error(key.attachment()+"Close of channel failed", ee);
}
} catch(CancelledKeyException e) {
final SelectionKey current = key;
//TODO: get rid of this if...else statement by fixing
//CancelledKeyException on linux so the tests don't fail
log.trace(() -> current.attachment()+"Processing of key failed, but continuing channel manager loop", e);
} catch(Throwable e) {
log.error(key.attachment()+"Processing of key failed, but continuing channel manager loop", e);
try {
key.cancel();
} catch(Throwable ee) {
log.info("cancelling key failed. exception type="+ee.getClass()+" msg="+ee.getMessage());
}
}
}
//clear the whole keySet as we processed them all in the while loop.
//If you do not clear the keySet, keys that have been already processed stay
//in the selected Key set. If another key gets added to this set, the selector
//goes off again and has the stale key plus the new key and the stale key
//is processed again.
keySet.clear();
}
private static void processKey(SelectionKey key, SelectorManager2 mgr, BufferPool pool) throws IOException, InterruptedException {
log.trace(() -> key.attachment()+"proccessing");
//This is code to try to avoid the CancelledKeyExceptions
if(!key.channel().isOpen() || !key.isValid())
return;
//if isAcceptable, than is a ServerSocketChannel
if(key.isAcceptable()) {
Helper.acceptSocket(key);
}
if(key.isConnectable())
Helper.connect(key);
if(key.isWritable()) {
Helper.write(key);
}
//The read MUST be after the write as a call to key.isWriteable is invalid if the
//read resulted in the far end closing the socket.
if(key.isReadable()) {
Helper.read(key, mgr, pool);
}
}
//each of these functions should be a handler for a new type that we set up
//on the outside of this thing. The signature is the same thing every time
// and we pass key and the Channel. We can cast to the proper one.
private static void acceptSocket(SelectionKey key) throws IOException {
// SelectableChannel s = key.channel();
log.trace(() -> key.attachment()+"Incoming Connection="+key);
WrapperAndListener struct = (WrapperAndListener)key.attachment();
BasTCPServerChannel channel = (BasTCPServerChannel)struct.getChannel();
channel.accept(channel.getChannelCount());
}
private static void connect(SelectionKey key) throws IOException {
log.trace(() -> key.attachment()+"finishing connect process");
WrapperAndListener struct = (WrapperAndListener)key.attachment();
CompletableFuture<Channel> callback = struct.getConnectCallback();
BasTCPChannel channel = (BasTCPChannel)struct.getChannel();
//must change the interests to not interested in connect anymore
//since we are connected otherwise selector seems to keep firing over
//and over again with 0 keys wasting cpu like a while(true) loop
int interests = key.interestOps();
key.interestOps(interests & (~SelectionKey.OP_CONNECT));
try {
channel.finishConnect();
callback.complete(channel);
} catch(Exception e) {
log.error(key.attachment()+"Could not open connection", e);
callback.completeExceptionally(e);
}
}
private static void read(SelectionKey key, SelectorManager2 mgr, BufferPool pool) throws IOException {
log.trace(() -> key.attachment()+"reading data");
WrapperAndListener struct = (WrapperAndListener)key.attachment();
DataListener in = struct.getDataHandler();
BasChannelImpl channel = (BasChannelImpl)struct.getChannel();
//if someone JUST unregistered for reads, then let's not read from this socket since it would put more
//pressure in RAM so just wait until they re-registerForReads and they will get the data then
if(!channel.isRegisteredForReads()) {
//log.info("not registered for reads so skipping");
return; //do not process reads if we were unregistered
}
ByteBuffer chunk = pool.nextBuffer(512);
try {
if(logBufferNextRead)
log.info(channel+"buffer="+chunk);
int bytes = channel.readImpl(chunk);
if(logBufferNextRead) {
logBufferNextRead = false;
log.info(channel+"buffer2="+chunk);
}
processBytes(key, chunk, bytes, mgr);
} catch(PortUnreachableException e) {
//this is a normal occurence when some writes out udp to a port that is not
//listening for udp!!! log as finer and fire to client to deal with it.
log.trace(() -> "Client sent data to a host or port that is not listening " +
"to udp, or udp can't get through to that machine", e);
in.failure(channel, null, e);
} catch(NotYetConnectedException e) {
//this happens in udp when I disconnect after someone had already been streaming me
//data. It is supposed to stop listening but selector keeps firing.
log.error("Can't read until UDPChannel is connected", e);
in.failure(channel, null, e);
} catch(IOException e) {
//kept getting the following exception so I added this as protection
//NOTE: this exceptionn should never occur. read should be returning -1
//but for some reason is returning hte exception instead.
// WARNING: [server] Processing of key failed, closing channel
// java.io.IOException: An established connection was aborted by the software in your host machine
// at sun.nio.ch.SocketDispatcher.read0(Native Method)
// at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:25)
// at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:233)
// at sun.nio.ch.IOUtil.read(IOUtil.java:206)
// at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:207)
// at biz.xsoftware.impl.nio.cm.basic.TCPChannelImpl.readImpl(TCPChannelImpl.java:168)
// at biz.xsoftware.impl.nio.cm.basic.Helper.read(Helper.java:128)
// at biz.xsoftware.impl.nio.cm.basic.Helper.processKey(Helper.java:77)
// at biz.xsoftware.impl.nio.cm.basic.Helper.processKeys(Helper.java:43)
// at biz.xsoftware.impl.nio.cm.basic.SelectorManager2.runLoop(SelectorManager2.java:262)
// at biz.xsoftware.impl.nio.cm.basic.SelectorManager2$PollingThread.run
// (SelectorManager2.java:224)
//another one that landes here is the Connection reset by peer"....
// Jan 18, 2012 1:00:42 PM biz.xsoftware.impl.nio.cm.basic.Helper read
// INFO: [stserver] Exception
// java.io.IOException: Connection reset by peer
// at sun.nio.ch.FileDispatcher.read0(Native Method)
// at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39)
// at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:251)
// at sun.nio.ch.IOUtil.read(IOUtil.java:224)
// at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:254)
// at biz.xsoftware.impl.nio.cm.basic.chanimpl.SocketChannelImpl.read(SocketChannelImpl.java:65)
// at biz.xsoftware.impl.nio.cm.basic.BasTCPChannel.readImpl(BasTCPChannel.java:108)
// at biz.xsoftware.impl.nio.cm.basic.Helper.read(Helper.java:162)
// at biz.xsoftware.impl.nio.cm.basic.Helper.processKey(Helper.java:104)
// at biz.xsoftware.impl.nio.cm.basic.Helper.processKeys(Helper.java:51)
// at biz.xsoftware.impl.nio.cm.basic.SelectorManager2.selectorFired(SelectorManager2.java:253)
// at biz.xsoftware.impl.nio.cm.basic.nioimpl.SelectorImpl.runLoop(SelectorImpl.java:126)
// at biz.xsoftware.impl.nio.cm.basic.nioimpl.SelectorImpl$PollingThread.run(SelectorImpl.java:107)
//One other exception starts with "An existing connection was forcibly closed"
//in the case of SSL over TCP only, sometimes instead of reading off a -1, this will
//throw an IOException: "An existing connection was forcibly closed by the remote host"
//we also close UDPChannels as well on IOException. Not sure if this is good or not.
process(key, mgr, in, channel, chunk, e);
} catch(NioException e) {
Throwable cause = e.getCause();
if(cause instanceof IOException) {
IOException ioExc = (IOException) cause;
process(key, mgr, in, channel, chunk, ioExc);
} else
throw e;
}
}
private static void process(SelectionKey key, SelectorManager2 mgr, DataListener in, BasChannelImpl channel,
ByteBuffer chunk, IOException e) throws IOException {
String msg = e.getMessage();
if(msg != null &&
(msg.contains("An existing connection was forcibly closed")
|| msg.contains("Connection reset by peer")
|| msg.contains("An established connection was aborted by the software in your host machine"))) {
log.trace(() -> "Exception 2", e);
processBytes(key, chunk, -1, mgr);
} else {
log.error("IO Exception unexpected", e);
in.failure(channel, null, e);
}
}
/**
* @param id
* @param b
* @param bytes
* @param mgr
* @throws IOException
*/
private static void processBytes(SelectionKey key, ByteBuffer data, int bytes, SelectorManager2 mgr) throws IOException
{
WrapperAndListener struct = (WrapperAndListener)key.attachment();
DataListener in = struct.getDataHandler();
BasChannelImpl channel = (BasChannelImpl)struct.getChannel();
ByteBuffer b = data;
b.flip();
if(bytes < 0) {
apiLog.trace(()->channel+"far end closed, cancel key, close socket");
channel.closeOnSelectorThread();
in.farEndClosed(channel);
} else if(bytes > 0) {
apiLog.trace(()->channel+"READ bytes="+bytes);
in.incomingData(channel, b);
}
}
private static void write(SelectionKey key) throws IOException, InterruptedException {
log.trace(()->key.attachment()+"writing data");
WrapperAndListener struct = (WrapperAndListener)key.attachment();
BasChannelImpl channel = (BasChannelImpl)struct.getChannel();
log.trace(()->channel+"notifying channel of write");
channel.writeAll();
}
static void unregisterSelectableChannel(RegisterableChannelImpl channel, int ops) {
SelectorManager2 mgr = channel.getSelectorManager();
if(!Thread.currentThread().equals(mgr.getThread()))
throw new RuntimeException(channel+"Bug, changing selector keys can only be done " +
"on registration thread because there is not synchronization");
//this could be dangerous and result in deadlock....may want
//to move this to the selector thread from jdk bugs!!!
//but alas, follow KISS, move on...
Select select = channel.getSelectorManager().getSelector();
SelectionKey key = channel.keyFor(select);
if(key == null || !key.isValid()) //no need to unregister, key is cancelled
return;
int previous = key.interestOps();
int opsNow = previous & ~ops; //subtract out the operation
key.interestOps(opsNow);
//log.info("unregistering="+Helper.opType(opsNow)+" opToSubtract="+Helper.opType(ops)+" previous="+Helper.opType(previous)+" type="+type);
//make sure we remove the appropriate listener and clean up
if(key.attachment() != null) {
WrapperAndListener struct = (WrapperAndListener)key.attachment();
struct.removeListener(ops);
}
}
}